/*
 * Copyright 2009 Alberto Gimeno <gimenete at gmail.com>
 *
 *   Licensed under the Apache License, Version 2.0 (the "License");
 *   you may not use this file except in compliance with the License.
 *   You may obtain a copy of the License at
 *
 *       http://www.apache.org/licenses/LICENSE-2.0
 *
 *   Unless required by applicable law or agreed to in writing, software
 *   distributed under the License is distributed on an "AS IS" BASIS,
 *   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *   See the License for the specific language governing permissions and
 *   limitations under the License.
 */
package siena.gae;

import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.List;
import java.util.Properties;

import siena.ClassInfo;
import siena.Json;
import siena.PersistenceManager;
import siena.Query;
import siena.SienaException;
import siena.Util;

import com.google.appengine.api.datastore.DatastoreService;
import com.google.appengine.api.datastore.DatastoreServiceFactory;
import com.google.appengine.api.datastore.Entity;
import com.google.appengine.api.datastore.Key;
import com.google.appengine.api.datastore.KeyFactory;
import com.google.appengine.api.datastore.Text;

public class GaePersistenceManager implements PersistenceManager {

	private DatastoreService ds;

	public void beginTransaction(int isolationLevel) {
	}

	public void closeConnection() {
	}

	public void commitTransaction() {
	}

	public <T> Query<T> createQuery(Class<T> clazz) {
		return new GaeQuery<T>(this, clazz);
	}

	public void delete(Object obj) {
		ds.delete(getKey(obj));
	}

	public void get(Object obj) {
		Key key = getKey(obj);
		try {
			Entity entity = ds.get(key);
			fillModel(obj, entity);
		} catch(Exception e) {
			throw new SienaException(e);
		}
	}

	public void init(Properties p) {
		ds = DatastoreServiceFactory.getDatastoreService();
	}

	public void insert(Object obj) {
		Class<?> clazz = obj.getClass();
		Entity entity = new Entity(ClassInfo.getClassInfo(clazz).tableName);
		fillEntity(obj, entity);
		ds.put(entity);
		setKey(ClassInfo.getIdField(clazz), obj, entity.getKey());
	}
	
	private void setKey(Field f, Object obj, Key key) {
		try {
			Object value = key.getId();
			if(f.getType() == String.class)
				value = value.toString();
			f.setAccessible(true);
			f.set(obj, value);
		} catch(Exception e) {
			throw new SienaException(e);
		}
	}

	public void rollbackTransaction() {
	}

	public void update(Object obj) {
		try {
			//			Entity entity = new Entity(getEntityName(obj.getClass()));
			//			entity.setProperty(Entity.KEY_RESERVED_PROPERTY, getKey(obj));
			Entity entity = ds.get(getKey(obj)); // FIXME don't read again
			fillEntity(obj, entity);
			ds.put(entity);
		} catch(SienaException e) {
			throw e;
		} catch(Exception e) {
			throw new SienaException(e);
		}
	}

	public Key getKey(Object obj) {
		try {
			Field f = ClassInfo.getIdField(obj.getClass());
			Object value = f.get(obj);
			if(value instanceof String)
				value = Long.parseLong((String) value); 
			return KeyFactory.createKey(ClassInfo.getClassInfo(obj.getClass()).tableName, (Long) value);
		} catch (Exception e) {
			throw new SienaException(e);
		}
	}

	private Object readField(Object object, Field field) {
		field.setAccessible(true);
		try {
			return field.get(object);
		} catch (Exception e) {
			throw new SienaException(e);
		}
	}

	private void fillEntity(Object obj, Entity entity) {
		Class<?> clazz = obj.getClass();

		for (Field field : ClassInfo.getClassInfo(clazz).updateFields) {
			String property = ClassInfo.getColumnNames(field)[0];
			Object value = readField(obj, field);
			Class<?> fieldClass = field.getType();
			if(ClassInfo.isModel(fieldClass)) {
				if(value == null) {
					entity.setProperty(property, null);
				} else {
					Key key = getKey(value);
					entity.setProperty(property, key);
				}
			} else {
				if(value != null && field.getType() == Json.class)
					value = value.toString();
				if(value instanceof String && value != null) {
					String s = (String) value;
					if(s.length() > 500)
						value = new Text(s);
				}
				Unindexed ui = field.getAnnotation(Unindexed.class);
				if(ui == null) {
					entity.setProperty(property, value);
				} else {
					entity.setUnindexedProperty(property, value);
				}
			}
		}
	}

	private void fillModel(Object obj, Entity entity) {
		Class<?> clazz = obj.getClass();

		for (Field field : ClassInfo.getClassInfo(clazz).updateFields) {
			field.setAccessible(true);
			String property = ClassInfo.getColumnNames(field)[0];
			try {
				Class<?> fieldClass = field.getType();
				if(ClassInfo.isModel(fieldClass)) {
					Key key = (Key) entity.getProperty(property);
					if(key != null) {
						Object value = fieldClass.newInstance();
						Field id = ClassInfo.getIdField(fieldClass);
						setKey(id, value, key);
						field.set(obj, value);
					}
				} else {
					setFromObject(obj, field, entity.getProperty(property));
				}
			} catch (Exception e) {
				throw new SienaException(e);
			}
		}
	}
	
	private void setFromObject(Object object, Field f, Object value)
		throws IllegalArgumentException, IllegalAccessException {
		if(value instanceof Text)
			value = ((Text) value).getValue();
		Util.setFromObject(object, f, value);
	}

	protected DatastoreService getDatastoreService() {
		return ds;
	}

	protected <T> List<T> mapEntities(List<Entity> entities, Class<T> clazz) {
		Field id = ClassInfo.getIdField(clazz);
		List<T> list = new ArrayList<T>(entities.size());
		for (Entity entity : entities) {
			T obj;
			try {
				obj = clazz.newInstance();
				fillModel(obj, entity);
				list.add(obj);
				setKey(id, obj, entity.getKey());
			} catch (SienaException e) {
				throw e;
			} catch (Exception e) {
				throw new SienaException(e);
			}
		}
		return list;
	}

}
